using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;
using System.Windows;
using System.Xml.Linq;

namespace N3P.WCF.Proxyless.FullDuplex.Silverlight
{
    /// <summary>
    /// Base class for full duplex WCF clients
    /// </summary>
    public abstract class WcfClientBase
    {

        /// <summary>
        /// The type of the service contract (interface)
        /// </summary>
        private readonly Type _contractType;
        /// <summary>
        /// The set of assemblies that have been loaded by the application at initialization
        /// </summary>
        private static readonly Assembly[] Assemblies;
        /// <summary>
        /// The set of named binding configurations that existed in ServiceReferences.ClientConfig at initialization
        /// </summary>
        internal static readonly Dictionary<string, Binding> BindingConfigurations;
        /// <summary>
        /// The set of known client definitions
        /// </summary>
        private static readonly IList<AddressBindingContract> EndpointConfigurations;
        /// <summary>
        /// The set of types that have been located by <see cref="FindType"/> cached by full name
        /// </summary>
        private static readonly Dictionary<string, Type> TypeCache;



        /// <summary>
        /// Creates a new <see cref="WcfClientBase"/>
        /// </summary>
        /// <param name="contractType">The <see cref="Type"/> of the service contract</param>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made into on response from the server</param>
        /// <param name="endpointName">The name of the endpoint configuration to use</param>
        /// <param name="remoteAddress">The address to access the service from</param>
        protected WcfClientBase(Type contractType, object callbackClass, string endpointName, string remoteAddress)
        {
            if (!contractType.IsInterface)
            {
                throw new ArgumentException("Contract type must be an interface", "contractType");
            }

            _contractType = contractType;
            CallbackClass = callbackClass;
            var sca = _contractType.GetCustomAttributes(typeof(ServiceContractAttribute), false).OfType<ServiceContractAttribute>().First();

            if (!sca.CallbackContract.IsInstanceOfType(callbackClass))
            {
                throw new ArgumentException("Callback class must implement " + sca.CallbackContract.FullName, "callbackClass");
            }

            CallbackType = sca.CallbackContract;
            var clientBase = EndpointConfigurations.First(x => x.Name == endpointName);
            Endpoint = new AddressBindingContract(new EndpointAddress(remoteAddress), clientBase.Binding, contractType);
            OpenChannel();
        }

        /// <summary>
        /// Creates a new <see cref="WcfClientBase"/>
        /// </summary>
        /// <param name="contractType">The <see cref="Type"/> of the service contract</param>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made into on response from the server</param>
        /// <param name="endpointName">The name of the endpoint configuration to use</param>
        /// <param name="remoteAddress">The address to access the service from</param>
        protected WcfClientBase(Type contractType, object callbackClass, string endpointName, EndpointAddress remoteAddress)
        {
            if (!contractType.IsInterface)
            {
                throw new ArgumentException("Contract type must be an interface", "contractType");
            }

            _contractType = contractType;
            CallbackClass = callbackClass;
            var sca = _contractType.GetCustomAttributes(typeof(ServiceContractAttribute), false).OfType<ServiceContractAttribute>().First();

            if (!sca.CallbackContract.IsInstanceOfType(callbackClass))
            {
                throw new ArgumentException("Callback class must implement " + sca.CallbackContract.FullName, "callbackClass");
            }

            CallbackType = sca.CallbackContract;
            var clientBase = EndpointConfigurations.First(x => x.Name == endpointName);
            Endpoint = new AddressBindingContract(remoteAddress, clientBase.Binding, contractType);
            OpenChannel();
        }

        /// <summary>
        /// Creates a new <see cref="WcfClientBase"/>
        /// </summary>
        /// <param name="contractType">The <see cref="Type"/> of the service contract</param>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made into on response from the server</param>
        /// <param name="binding">The binding to use</param>
        /// <param name="remoteAddress">The address to access the service from</param>
        protected WcfClientBase(Type contractType, object callbackClass, Binding binding, EndpointAddress remoteAddress)
        {
            if (!contractType.IsInterface)
            {
                throw new ArgumentException("Contract type must be an interface", "contractType");
            }

            _contractType = contractType;
            CallbackClass = callbackClass;
            var sca = _contractType.GetCustomAttributes(typeof(ServiceContractAttribute), false).OfType<ServiceContractAttribute>().First();

            if (!sca.CallbackContract.IsInstanceOfType(callbackClass))
            {
                throw new ArgumentException("Callback class must implement " + sca.CallbackContract.FullName, "callbackClass");
            }

            CallbackType = sca.CallbackContract;
            Endpoint = new AddressBindingContract(remoteAddress, binding, contractType);
            OpenChannel();
        }

        /// <summary>
        /// Creates a new <see cref="WcfClientBase"/>
        /// </summary>
        /// <param name="contractType">The <see cref="Type"/> of the service contract</param>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made into on response from the server</param>
        /// <param name="endpointName">The name of the endpoint configuration that should be used</param>
        protected WcfClientBase(Type contractType, object callbackClass, string endpointName)
        {
            if (!contractType.IsInterface)
            {
                throw new ArgumentException("Contract type must be an interface", "contractType");
            }

            _contractType = contractType;
            CallbackClass = callbackClass;
            var sca = _contractType.GetCustomAttributes(typeof(ServiceContractAttribute), false).OfType<ServiceContractAttribute>().First();

            if (!sca.CallbackContract.IsInstanceOfType(callbackClass))
            {
                throw new ArgumentException("Callback class must implement " + sca.CallbackContract.FullName, "callbackClass");
            }

            CallbackType = sca.CallbackContract;
            Endpoint = EndpointConfigurations.First(x => x.Contract == _contractType && x.Name == endpointName);
            OpenChannel();
        }

        /// <summary>
        /// Creates a new <see cref="WcfClientBase"/>
        /// </summary>
        /// <param name="contractType">The <see cref="Type"/> of the service contract</param>
        /// <param name="callbackClass">The instance of the callback contract that calls should be made into on response from the server</param>
        protected WcfClientBase(Type contractType, object callbackClass)
        {
            if(!contractType.IsInterface)
            {
                throw new ArgumentException("Contract type must be an interface", "contractType");
            }

            _contractType = contractType;
            CallbackClass = callbackClass;
            var sca = _contractType.GetCustomAttributes(typeof (ServiceContractAttribute), false).OfType<ServiceContractAttribute>().First();
            
            if(!sca.CallbackContract.IsInstanceOfType(callbackClass))
            {
                throw new ArgumentException("Callback class must implement " + sca.CallbackContract.FullName, "callbackClass");
            }

            CallbackType = sca.CallbackContract;
            Endpoint = EndpointConfigurations.First(x => x.Contract == _contractType);
            OpenChannel();
        }

        /// <summary>
        /// One time necessary evil - pulls all the known binding configurations and endpoints from the ServiceReferences.ClientConfig
        /// </summary>
        static WcfClientBase()
        {
            BindingConfigurations = new Dictionary<string, Binding>();
            EndpointConfigurations = new List<AddressBindingContract>();
            Assemblies = AppDomain.CurrentDomain.GetAssemblies();
            TypeCache = new Dictionary<string, Type>();

            var resStream = Application.GetResourceStream(new Uri("ServiceReferences.ClientConfig", UriKind.Relative)).Stream;
            var doc = XDocument.Load(resStream);
            var docElements = doc.Elements().SelectMany(x => x.Elements()).SelectMany(x => x.Elements()).ToList();
            var bindingTypes = docElements.First(x => x.Name == "bindings").Elements().ToList();

            foreach(var bindingType in bindingTypes)
            {
                var bindingTypeName = UppercaseFirstChar(bindingType.Name.LocalName);
                var elementList = new List<BindingElement>();
                var bindingInstances = bindingType.Elements();

                foreach (var bindingInstance in bindingInstances)
                {
                    var bindingElements = bindingInstance.Elements();

                    foreach (var bindingElement in bindingElements)
                    {
                        var bindingElementType = UppercaseFirstChar(bindingElement.Name.LocalName + "BindingElement");
                        var element = (BindingElement)Activator.CreateInstance(FindType(bindingElementType));

                        foreach (var attribute in bindingElement.Attributes())
                        {
                            var propertyName = UppercaseFirstChar(attribute.Name.LocalName);
                            var property = element.GetType().GetProperty(propertyName);
                            var propertyType = property.PropertyType;
                            property.SetValue(element, Convert.ChangeType(attribute.Value, propertyType, null), null);
                        }

                        elementList.Add(element);
                    }

                    var binding = (Binding) Activator.CreateInstance(FindType(bindingTypeName), new object[0]);
                    var nameAttribute = bindingInstance.Attributes().FirstOrDefault(x => x.Name.LocalName == "name");
                    var name = String.Empty;

                    if (nameAttribute != null)
                    {
                        name = nameAttribute.Value;
                    }

                    foreach (var attribute in bindingInstance.Attributes())
                    {
                        var propertyName = UppercaseFirstChar(attribute.Name.LocalName);
                        var property = binding.GetType().GetProperty(propertyName);
                        var propertyType = property.PropertyType;
                        var value = propertyType != typeof (TimeSpan)
                                        ? Convert.ChangeType(attribute.Value, propertyType, null)
                                        : TimeSpan.Parse(attribute.Value);
                        property.SetValue(binding, value, null);
                    }

                    BindingConfigurations[name] = binding;
                }
            }

            var clientChildren = docElements.First(x => x.Name == "client").Elements().ToList();

            foreach (var endpointElement in clientChildren)
            {
                var attributes = endpointElement.Attributes().ToList();
                var address = attributes.First(x => x.Name == "address").Value;
                var binding = attributes.First(x => x.Name == "binding").Value;
                var name = attributes.FirstOrDefault(x => x.Name == "name");
                var bindingConfiguration = attributes.FirstOrDefault(x => x.Name == "bindingConfiguration");
                var contract = attributes.First(x => x.Name == "contract").Value;

                EndpointConfigurations.Add(new AddressBindingContract(address, contract, binding,
                                                       bindingConfiguration != null ? bindingConfiguration.Value : null,
                                                       name != null ? name.Value : null));
            }
        }



        /// <summary>
        /// Gets the instance of the callback contract that will be called back into by the server
        /// </summary>
        protected object CallbackClass { get; private set; }

        /// <summary>
        /// Gets the type of the callback contract
        /// </summary>
        protected Type CallbackType { get; private set; }

        /// <summary>
        /// Gets the <see cref="IDuplexChannel"/> that will be used for communication to and from the server
        /// </summary>
        protected IDuplexChannel Channel { get; private set; }

        /// <summary>
        /// Gets or sets the definition of the endpoint for the service
        /// </summary>
        private AddressBindingContract Endpoint { get; set; }

        /// <summary>
        /// Gets the <see cref="MessageVersion"/> currently being used by the <see cref="Channel"/>
        /// </summary>
        protected MessageVersion MessageVersion
        {
            get
            {
                return Channel.GetProperty<MessageVersion>();
            }
        }




        /// <summary>
        /// Aborts and re-opens the underlying communications channel
        /// </summary>
        public void Reset()
        {
            Channel.Abort();
            OpenChannel();
        }


        /// <summary>
        /// Sends a message over the <see cref="Channel"/>
        /// </summary>
        /// <param name="message">The <see cref="Message"/> to send</param>
        /// <seealso cref="FinishSend"/>
        protected void Send(Message message)
        {
            TimerCallback sendAct = x => Channel.BeginSend(message, FinishSend, null);

            try
            {
                sendAct(null);
            }
            catch
            {
                new Timer(sendAct).Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
            }

        }


        /// <summary>
        /// The handler for <see cref="IDuplexChannel.EndOpen"/>
        /// </summary>
        /// <param name="ar">The <see cref="IAsyncResult"/> from <see cref="IDuplexChannel.BeginOpen(AsyncCallback, object)"/></param>
        /// <seealso cref="OpenChannel"/>
        private void ChannelOpened(IAsyncResult ar)
        {
            try
            {
                Channel.EndOpen(ar);
            }
            catch
            {
                new Timer(x => OpenChannel()).Change(TimeSpan.FromSeconds(1), TimeSpan.FromMilliseconds(-1));
            }

            Receiver();
        }

        /// <summary>
        /// The handler for <see cref="IDuplexChannel.EndSend"/>
        /// </summary>
        /// <param name="ar">The <see cref="IAsyncResult"/> from <see cref="IDuplexChannel.BeginSend(Message, AsyncCallback, object)"/></param>
        /// <seealso cref="Send"/>
        private void FinishSend(IAsyncResult ar)
        {
            try
            {
                Channel.EndSend(ar);
            }
            catch
            {
                Reset();
            }
        }

        /// <summary>
        /// Starts opening the underlying <see cref="Channel"/>
        /// </summary>
        /// <seealso cref="ChannelOpened"/>
        private void OpenChannel()
        {
            var binding = Endpoint.Binding;
            var factory = binding.BuildChannelFactory<IDuplexSessionChannel>();
            factory.Open();
            Channel = factory.CreateChannel(Endpoint.Address);
            Channel.BeginOpen(ChannelOpened, null);
        }

        /// <summary>
        /// Handler for <see cref="IDuplexChannel.EndReceive"/>
        /// </summary>
        /// <param name="result">The <see cref="IAsyncResult"/> from <see cref="IDuplexChannel.BeginReceive(AsyncCallback, object)"/></param>
        /// <seealso cref="Receiver"/>
        private void ReceiveComplete(IAsyncResult result)
        {
            try
            {
                var receivedMessage = Channel.EndReceive(result);

                // Show the service response in the UI.
                if (receivedMessage != null)
                {
                    MessageHelper.PerformCallback(CallbackClass, CallbackType, receivedMessage);
                }

                Receiver();
            }
            catch
            {
                Channel.Abort();
                OpenChannel();
            }
        }

        /// <summary>
        /// Attempts to receive from the unerlying <seealso cref="Channel"/> as long as the channel is open
        /// </summary>
        /// <seealso cref="ReceiveComplete"/>
        private void Receiver()
        {
            if(Channel.State == CommunicationState.Opened)
            {
                Channel.BeginReceive(ReceiveComplete, null);
            }
        }

        /// <summary>
        /// Uppercases the first character in a string
        /// </summary>
        /// <param name="str">The string to uppercase the first character of</param>
        /// <returns>The string with the first character uppercased</returns>
        private static string UppercaseFirstChar(string str)
        {
            return str.Substring(0, 1).ToUpper() + str.Substring(1);
        }


        /// <summary>
        /// Locates a <seealso cref="Type"/> by full name by searching 1) the local cache 2) every loaded assembly
        /// </summary>
        /// <param name="name">The name of the <seealso cref="Type"/> to locate</param>
        /// <returns>The located <seealso cref="Type"/> or null if not found</returns>
        internal static Type FindType(string name)
        {
            if(!TypeCache.ContainsKey(name))
            {
                var proposedType = Assemblies.Select(x => x.GetType(name)).FirstOrDefault(x => x != null);
                proposedType = proposedType ?? Assemblies.SelectMany(x => x.GetTypes().Where(y => y.Name == name)).FirstOrDefault(x => x != null);
                return TypeCache[name] = proposedType;
            }
            return TypeCache[name];
        }
    }
}